/* Copyright (c) 2008 Charles Rich and Worcester Polytechnic Institute.
 * All Rights Reserved.  Use is subject to license terms.  See the file
 * "license.terms" for information on usage and redistribution of this
 * file and for a DISCLAIMER OF ALL WARRANTIES.
 */
package edu.wpi.cetask;

import org.w3c.dom.*;

import java.util.*;
import java.util.regex.*;

import javax.xml.xpath.*;

public class DecompositionClass extends TaskModel.Member 
                                implements Description.Taskable {  
   
   // TODO check for duplicate step names
   
   // do not confuse with JavaScript bindings in Instance
   private final Map<String,Binding> bindings = new HashMap<String,Binding>();
   
   private final static Pattern pattern = // to match $step.slot
      Pattern.compile("\\$\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*");      

   private final TaskClass goal;
   
   public TaskClass getTask () { return goal; }  // Taskable  
   
   public Boolean isApplicable (Task goal) {
      return goal.evalCondition(xpath("./applicable"), this+" applicable");
   }
      
   DecompositionClass (Node node, TaskModel model, TaskClass goal) { 
      model.super(node);
      this.goal = goal == null ? resolveTaskClass(xpath("./@goal")) : goal;
      // analyze and store binding dependencies
      NodeList nodes = (NodeList) xpath("./binding", XPathConstants.NODESET);
      try { 
         for (int i = 0; i < nodes.getLength(); i++) { // preserve order
            Node bindingNode = nodes.item(i);
            String variable = XPATH.evaluate("./@slot", bindingNode); 
            if ( bindings.get(variable) != null )
               throw new TaskModel.Error(this,
                     "duplicate bindings for "+variable);
            bindings.put(variable, new Binding(variable, bindingNode));
         }
         for (Binding binding : bindings.values()) {
            Matcher matcher = pattern.matcher(binding.value);
            while ( matcher.find() ) {
               Binding depend = bindings.get(matcher.group());
               if ( depend == null ) {
                  if ( binding.identity == true )
                     // special case for identity w/o next level binding
                     binding.depends.add(new Binding(binding.value, null));
               } else binding.depends.add(depend);
            }
         }
      } catch (Exception e) { throw new RuntimeException(e); }
   }

   public TaskClass getGoal () { return goal; } 
   
   public boolean isOrdered () {
      String ordered = xpath("./@ordered");
      return ordered.length() == 0 || Boolean.parseBoolean(ordered);
   }
   
   public List<String> getStepNames () { 
      return xpathValues("./step/@name"); 
   }
   
   public List<TaskClass> getStepTypes () { 
      List<TaskClass> types = new ArrayList<TaskClass>();
      for (String qname : xpathValues("./step/@task")) 
         types.add(resolveTaskClass(qname));
      return types;
   }
   
   public TaskClass getStepType (String name) {
      return resolveTaskClass(xpath("./step[@name=\""+name+"\"]/@task"));
   }
   
   /**
    * Test whether named step in this decomposition class is optional.
    */
   public boolean isOptionalStep (String name) {
      String minOccurs = xpath("./step[@name=\""+name+"\"]/@minOccurs");
      return minOccurs.length() > 0 && Integer.parseInt(minOccurs) == 0;
   }
   
   /**
    * Return the maximum number of times named step in this decomposition
    * class may be repeated, or -1 if unbounded.
    */
   public int getMaxOccursStep (String name) {
      String maxOccurs = xpath("./step[@name=\""+name+"\"]/@maxOccurs");
      return maxOccurs.length() == 0 ? 1 : 
         maxOccurs.equals("unbounded") ? -1 : Integer.parseInt(maxOccurs);
   }
   
   public List<String> getRequiredStepNames (String name) {
      StringTokenizer tokenizer= 
         new StringTokenizer(xpath("./step[@name=\""+name+"\"]/@requires"));
      List<String> required = new ArrayList<String>();
      while (tokenizer.hasMoreTokens()) 
         required.add(tokenizer.nextToken());
      if ( isOrdered() ) {
         List<String> steps = getStepNames();
         int i = steps.indexOf(name);
         if ( i > 0 ) required.add(steps.get(i-1));
      }
      return required;
   }
  
   void updateBindings (Decomposition decomp) {
      for (Binding binding : bindings.values()) 
         binding.update(decomp, 0);
   }
   
   /**
    * Maximum allowed depth of dependency chain in bindings (default 10). Used
    * to protect against cyclic dependencies.
    */
   public static int MAX_BINDING_DEPTH = 10;  
   
   private class Binding {
      private final String variable, step, slot, value, where;
      private final boolean identity, inIn;
      private final Node node;

      private final List<Binding> depends = new ArrayList<Binding>();

      private Binding (String variable, Node node) 
         throws XPathExpressionException {
         this.variable = variable;
         StringTokenizer tokenizer = new StringTokenizer(
               variable.substring(1), ".");
         step = tokenizer.nextToken();
         if ( !(step.equals("this") || getStepNames().contains(step)) )
            throw new TaskModel.Error(DecompositionClass.this,
                  "slot binding \""+variable+
                  "\" does not refer to any step of subtasks");
         slot = tokenizer.nextToken();
         this.node = node;
         if ( node == null ) { value = null; identity = inIn = false; }
         else {
            value = XPATH.evaluate("./@value", node);
            identity = pattern.matcher(value).matches();
            // important special case of binding step input to goal input
            inIn = identity && value.startsWith("$this.") &&
              !step.equals("this") &&
              getGoal().getInputNames().contains(value.substring(6)) &&
              getStepType(step).getInputNames().contains(slot);
         }
         where = DecompositionClass.this + " binding for " + variable;
      }

      // Note this implements only monotonic propagation, not full
      // dependency-based constraint propagation
      private void update (Decomposition decomp, int depth) {
         if ( depth > MAX_BINDING_DEPTH )
            throw new IllegalStateException(where + " stopped at depth "+ depth);
         depth++;
         for (Binding depend : depends) 
            depend.update(decomp, depth); // recursive
         for (Binding depend : depends) {
            Task task = getStep(decomp, depend.step); 
            if ( task == null )
               throw new RuntimeException("Unknown slot "+depend.variable
                     +" in "+DecompositionClass.this+" binding for "+variable);
            if ( !task.isDefinedSlot(depend.slot) ) {
               if ( inIn ) { // special case for inIn identity only
                  if ( getStep(decomp, step).isDefinedSlot(slot) ) 
                     // assign other direction
                     task.setSlotValueScript(depend.slot, variable, where,
                           decomp.bindings);
               }
               // don't try to compute non-identity binding that depends on undefined
               // variables, since cannot guarantee that value expression will be
               // safe (strict), but *do* propagate undefined through identities
               // since only go "forward" (causality) except for inIn's; this
               // an approximation to what full constraint propagation would do
               if ( !identity ) return;
            } else if ( identity && getStep(decomp, step).isDefinedSlot(slot)
                  // compare values in JavaScript
                  && !decomp.evalCondition(variable+" == "+depend.variable, where) )
               throw new TaskModel.Error(DecompositionClass.this,
                     "Contradiction involving "+this+" and "+depend);
         }
         if ( value != null )
            getStep(decomp, step).setSlotValueScript(slot, value, where,
                  decomp.bindings);
      }
      
      private Task getStep (Decomposition decomp, String step) {
         return step.equals("this") ? decomp.getGoal() :
            decomp.getStep(step); 
      }
      
      @Override
      public String toString () { return variable; }
   }
}

